<?php

/**
 * @see Zend_Http_Client
 */
require_once 'Zend/Http/Client.php';

/**
 * @see Zend_Loader
 */
require_once 'Zend/Loader.php';

/**
 * Calcula o valor de um frete utilizando o sistema dos correios.
 *
 * www.correios.com.br
 *
 * @category Tavs
 * @package Tavs_Frete
 *
 * @author Tales Santos <tales.augusto.santos>
 */
abstract class Tavs_Frete
{
	/**
	 * Servicos sem contrato
	 */
	const SERVICO_SEM_CONTRATO_SEDEX 	    = 40010;
	const SERVICO_SEM_CONTRATO_SEDEX_COBRAR = 40045;
	const SERVICO_SEM_CONTRATO_SEDEX10		= 40215;
	const SERVICO_SEM_CONTRATO_SEDEX_HOJE	= 40290;
	const SERVICO_SEM_CONTRATO_PAC   		= 41106;

	/**
	 * Servicos com contrato
	 */
	const SERVICO_COM_CONTRATO_SEDEX		= 40096;
	const SERVICO_COM_CONTRATO_SEDEX_COBRAR	= 40436;
	const SERVICO_COM_CONTRATO_SEDEX10		= 40444;
	const SERVICO_COM_CONTRATO_ESEDEX		= 81019;
	const SERVICO_COM_CONTRATO_PAC			= 41068;

	/**
	 * Formato da encomenda
	 */
	const ENCOMENDA_CAIXA_PACOTE = 1;
	const ENCOMENDA_ROLO_PRIMSA  = 2;

	/**
	 * Tipo de retorno da requisicao
	 */
	const RETORNO_XML 	= 'XML';
	const RETORNO_POPUP = 'Popup';

	/**
	 * Endereco para o servico de calculo de frete
	 *
	 * @var string
	 */
	protected $_uriCorreios = 'http://shopping.correios.com.br/wbm/shopping/script/CalcPrecoPrazo.aspx';

	/**
	 * CEP de origem sem formatacao
	 *
	 * @var string
	 */
	protected $_cepOrigem;

	/**
	 * CEP de destino sem formatacao
	 *
	 * @var string
	 */
	protected $_cepDestino;

	/**
	 * Codigo administrativo
	 *
	 * @var string
	 */
	protected $_empresa = '';

	/**
	 * Senha para acesso ao servico
	 *
	 * @var string
	 */
	protected $_senha = '';

	/**
	 * Codigos dos servicos.
	 *
	 * @var array
	 */
	protected $_servicos = array();

	/**
	 * Peso da encomenda incluindo sua embalagem.
	 *
	 * @var float
	 */
	protected $_encomendaPeso;

	/**
	 * Comprimento da encomenda
	 *
	 * @var int
	 */
	protected $_encomendaFormato = 1;

	/**
	 * Comprimento da encomenda.
	 *
	 * @var float
	 */
	protected $_encomendaComprimento = 0;

	/**
	 * Altura da encomenda.
	 *
	 * @var float
	 */
	protected $_encomendaAltura = 0;

	/**
	 * Largura da encomenda.
	 *
	 * @var float
	 */
	protected $_encomendaLargura = 0;

	/**
	 * Diametro da encomenda.
	 *
	 * @var float
	 */
	protected $_encomendaDiametro = 0;

	/**
	 * Indica se a encomenda sera entregue com servico
	 * adicional de mao propria.
	 *
	 * @var string
	 */
	protected $_maoPropria = 'N';

	/**
	 * Indica se a encomenda sera entregue com o servico
	 * adicional valor declarado.
	 *
	 * @var float
	 */
	protected $_valorDeclarado = 0;

	/**
	 * Indica se a encomenda será entregue com o serviço
	 * adicional aviso de recebimento.
	 *
	 * @var string
	 */
	protected $_avisoRecebimento = 'N';

	/**
	 * Mapeamento dos parametros e atributos
	 *
	 * @var array
	 */
	protected $_paramsMap = array(

		'_empresa' => array(
			'paramName' => 'nCdEmpresa',
			'required'  => false
		),

		'_senha' => array(
			'paramName' => 'sDsSenha',
			'required'  => false
		),

		'_servicos' => array(
			'paramName' => 'nCdServico',
			'required'	=> true
		),

		'_cepOrigem' => array(
			'paramName'=> 'sCepOrigem',
			'required' => true
		),

		'_cepDestino' => array(
			'paramName'=> 'sCepDestino',
			'required' => true
		),

		'_encomendaPeso' => array(
			'paramName' => 'nVlPeso',
			'required'  => true
		),

		'_encomendaFormato' 	=> array(
			'paramName' => 'nCdFormato',
			'required'  => true
		),

		'_encomendaComprimento' => array(
			'paramName'=> 'nVlComprimento',
			'required' => true
		),

		'_encomendaAltura' => array(
			'paramName'=> 'nVlAltura',
			'required' => true
		),

		'_encomendaDiametro' => array(
			'paramName'=> 'nVlDiametro',
			'required' => true
		),

		'_maoPropria' => array(
			'paramName' => 'sCdMaoPropria',
			'required'  => true
		),

		'_valorDeclarado' => array(
			'paramName' => 'nVlValorDeclarado',
			'required'  => true
		),

		'_avisoRecebimento' => array(
			'paramName' => 'sCdAvisoRecebimento',
			'required'  => true
		),

		'_retorno' => array(
			'paramName' => 'StrRetorno',
			'required' => true
		)

	);

	/**
	 * Constructor.
	 *
	 * @param string $cepOrigem CEP de origem
	 * @param string $cepDestino CEP de destino
	 */
	public function __construct($cepOrigem, $cepDestino)
	{
		$this->_setCep($cepOrigem,  'origem');
		$this->_setCep($cepDestino, 'destino');
	}

	/**
	 * Constroi um objeto utilizando o formato de retorno
	 *
	 * @param string $format Formato a ser retornado
	 * @param string $cepOrigem Cep de origem
	 * @param string $cepDestino Cep de destino
	 *
	 * @return Tavs_Frete
	 */
	static public function factory($format, $cepOrigem, $cepDestino)
	{
		$className = 'Tavs_Frete_Retorno_' . ucfirst($format);

		try {
			Zend_Loader::loadClass($className);
		}
		catch (Zend_Exception $e) {
			throw new Tavs_Frete_Exception('tipo de retorno -' . $className . '- nao foi implementado ainda');
		}

		$adapter = new $className($cepOrigem, $cepDestino);

		if (!$adapter instanceof Tavs_Frete) {
			throw new Tavs_Frete_Exception('o adapter deve ser uma instancia de Tavs_Frete');
		}

		return $adapter;
	}

	/**
	 * Seta o CEP.
	 *
	 * @param string $cep CEP a ser setado
	 * @param string $tipo Tipo do CEP a ser setado [origem|destino]
	 *
	 * @return Tavs_Frete
	 */
	final private function _setCep($cep, $tipo)
	{
		$search = array('.', '-', ' ');
		$cep = str_replace($search, '', $cep);

		if (preg_match('/\d\d\d\d\d\d\d\d/', $cep)) {
			$property = '_cep' . ucfirst($tipo);
			if (property_exists($this, $property)) {
				$this->{$property} = $cep;
			}
			else {
				throw new Tavs_Frete_Exception('tipo de cep nao existe: ' . $tipo);
			}
		}
		else {
			throw new Tavs_Frete_Exception('cep com formato invalido: ' . $cep);
		}
	}

	/**
	 * Seta o codigo administrativo junto a ECT.
	 *
	 * O código está disponível no corpo do contrato
	 * firmado com os Correios
	 *
	 * @param string $numId Codigo administrativo
	 *
	 * @return Tavs_Frete
	 */
	public function setEmpresa($numId)
	{
		$this->_empresa = (string) $numId;
		return $this;
	}

	/**
	 * Seta a senha de acesso ao servico.
	 *
	 * @param string $numId Codigo administrativo
	 *
	 * @return Tavs_Frete
	 */
	public function setSenha($senha)
	{
		$this->_senha = (string) $senha;
		return $this;
	}

	/**
	 * Adiciona um ou mais servicos a consulta
	 *
	 * @param int|array $servico Um ou varios servicos
	 *
	 * @return Tavs_Frete
	 */
	public function addServico($servico)
	{
		if (is_array($servico)) {
			foreach ($servico as $servicoId) {
				$this->addServico($servicoId);
			}
		}
		else {

			$servico = (int) $servico;
			$allowed_services = $this->_getConstants('SERVICO_');

			if (in_array($servico, $allowed_services)) {
				$this->_servicos[] = $servico;
			}
			else {
				throw new Tavs_Frete_Exception('servico nao disponivel ainda ');
			}
		}

		return $this;
	}

	/**
	 * Retorna as constantes em forma de array
	 *
	 * @param string $prefix Prefixo a ser procurado
	 *
	 * @return array
	 */
	protected function _getConstants($prefix)
	{
		//le as constantes de servico
		$reflection = new ReflectionClass($this);
		$constants = $reflection->getConstants();
		$allowed_services = array();

		foreach ($constants as $constName => $constValue) {
			$constPrefix = substr($constName, 0, strlen($prefix));
			if ($constPrefix == $prefix) {
				$allowed_services[] = $constValue;
			}
		}

		return $allowed_services;
	}

	/**
	 * Peso da encomenda incluindo sua embalagem.
	 * Deve ser fornecida em quilogramas (KG).
	 *
	 * @param float $peso Peso da encomenda
	 *
	 * @return Tavs_Frete
	 */
	public function setEncomendaPeso($peso)
	{
		$this->_encomendaPeso = (float)$peso;
		return $this;
	}

	/**
	 * Seta o formato da encomenda.
	 *
	 * Valor 1 para caixa/pacote
	 * Valor 2 para rolo/prisma
	 *
	 * @param int $formato Tipo do formato
	 *
	 * @return Tavs_Frete
	 */
	public function setEncomendaFormato($formato)
	{
		$formato = (int) $formato;
		$formatAlloweds = $this->_getConstants('ENCOMENDA_');

		if (in_array($formato, $formatAlloweds)) {
			$this->_encomendaFormato = $formato;
			return $this;
		}
		else {
			throw new Tavs_Frete_Exception('formato da encomenda nao disponivel ainda');
		}
	}

	/**
	 * Comprimento da encomenda incluindo a embalagem.
	 *
	 * Requerido somente se o servico exigir.
	 *
	 * @param float $comprimento Comprimento da encomenda
	 *
	 * @return Tavs_Frete
	 */
	public function setEncomendaComprimento($comprimento)
	{
		$this->_encomendaComprimento = (float)$comprimento;
		return $this;
	}

	/**
	 * Altura da encomenda incluindo a embalagem.
	 *
	 * Requerido somente se o servico exigir.
	 *
	 * @param float $altura Altura da encomenda
	 *
	 * @return Tavs_Frete
	 */
	public function setEncomendaAltura($altura)
	{
		$this->_encomendaAltura = (float)$altura;
		return $this;
	}

	/**
	 * Largura da encomenda incluindo a embalagem.
	 *
	 * Requerido somente se o servico exigir.
	 *
	 * @param float $largura Largura da encomenda
	 *
	 * @return Tavs_Frete
	 */
	public function setEncomendaLargura($largura)
	{
		$this->_encomendaLargura = (float)$largura;
		return $this;
	}

	/**
	 * Diametro da encomenda incluindo a embalagem.
	 *
	 * Requerido somente se o servico exigir.
	 *
	 * @param float $diametro Diametro da encomenda
	 *
	 * @return Tavs_Frete
	 */
	public function setEncomendaDiametro($diametro)
	{
		$this->_encomendaDiametro = (float)$diametro;
		return $this;
	}

	/**
	 * Indica se a encomenda sera entregue com servico
	 * adicional de mao propria.
	 *
	 * @param bool $bool Mao propria?
	 *
	 * @return Tavs_Frete
	 *
	 */
	public function setMaoPropria($bool = true)
	{
		$this->_maoPropria = ((bool) $bool) ? 'S' : 'N';
		return $this;
	}

	/**
	 * Indica se a encomenda sera entregue com o servico
	 * adicional valor declarado.
	 *
	 * @param bool $bool Mao propria?
	 *
	 * @return Tavs_Frete
	 *
	 */
	public function setValorDeclarado($valor)
	{
		$this->_valorDeclarado = (float) $valor;
		return $this;
	}

	/**
	 * Indica se a encomenda será entregue com o serviço
	 * adicional aviso de recebimento.
	 *
	 * @param bool $bool Aviso recebimento?
	 *
	 * @return Tavs_Frete
	 *
	 */
	public function setAvisoRecebimento($bool = true)
	{
		$this->_avisoRecebimento = ((bool) $bool) ? 'S' : 'N';
		return $this;
	}

	/**
	 * Realiza o calculo do frete.
	 *
	 * @return Tavs_Frete_Resultado
	 */
	public function calcular()
	{
		$params = $this->_getParams();

		//configura a requisicao
		$request = new Zend_Http_Client($this->_uriCorreios);
		$request->setParameterGet($params);

		//obtem a resposta
		$response = $request->request();

		//adapter le a resposta
		$resultado = $this->_readResponse($response);

		if (!$resultado instanceof Tavs_Frete_Resultado) {
			$this->_throwException('retorno da consulta deve ser instancia de Tavs_Frete_Resultado. retorno foi ' . get_class($resultado));
		}

		return $resultado;
	}

	private function _preConsulta(array $params)
	{
		//parametro servico deve ser string separado por ,
		$serviceParamName = $this->_paramsMap['_servicos']['paramName'];
		if (array_key_exists($serviceParamName, $params)) {
			$params[$serviceParamName] = implode(',', $params[$serviceParamName]);
		}

		return $params;
	}

	/**
	 * Retorna os parametros para envio.
	 *
	 * @return array
	 */
	protected function _getParams()
	{
		$params = array();

		foreach ($this->_paramsMap as $attr => $paramOptions)
		{
			if (property_exists($this, $attr))
			{
				$attrValue = $this->{$attr};

				//e requerido, nao esta vazio e nao e nulo OU
				//nao e requerido
				if (($paramOptions['required'] === true && (string)$attrValue != '' && null !== $attrValue) ||
					($paramOptions['required'] === false)) {
					$params[$paramOptions['paramName']] = $attrValue;
				}
				else {
					$this->_throwException('propriedade ' . $attr . ' e requerida mas nao foi setado valor');
				}
			}
			else {
				$this->_throwException('propriedade ' . $attr . ' foi mapeada mas nao existe na classe');
			}
		}

		//organiza os parametros
		$params = $this->_preConsulta($params);

		return $params;
	}

	/**
	 * Lanca excecoes.
	 *
	 * @param msg $msg Mensagem a ser lancada na excecao
	 *
	 * @return void
	 */
	private function _throwException($msg, $code = null)
	{
		throw new Tavs_Frete_Exception($msg, $code);
	}

	/**
	 * Le a resposta da requisicao
	 *
	 * @param Zend_Http_Response $response Contem a resposta da requisicao
	 *
	 * @return Tavs_Response Objeto com a resposta do servidor
	 */
	abstract protected function _readResponse(Zend_Http_Response $response);

}